/*
 * AbstractDataBean.java
 *
 * Created on 30 May 2007, 16:01
 *
 * Copyright (C)
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 2 of the License, or (at your option) any later
 * version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
 * Place, Suite 330, Boston, MA 02111-1307 USA
 */

package com.grubby.databean;

import com.grubby.databean.annotation.SqlTableName;
import com.grubby.databean.annotation.SqlColumnName;
import com.grubby.databean.constraints.annotation.ConstrainedClass;
import com.grubby.databean.exception.ValidationException;
import com.grubby.utils.StringUtils;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * This abstract class represents persistence level constructs such as a 
 * database table or LDAP node.  Simple methods similar to toString are provided 
 * to output the bean's properties as a SQL-99 INSERT statement, a CSV list, 
 * (and an LDIF node element - to come)
 * <P>
 * Additional meta information can be provided about the target persistence 
 * store via annotations such as SqlTableName and SqlColumnName.  However, if 
 * these are not provided in the extended class then sensible defaults are 
 * calculated instead based on the class and field names.
 * <P>
 * Finally, a validate() method is provided.  This checks that all properties 
 * whose setters are annotated with constraints such as @Mandatory or 
 * @MaxLength(10) have values which are valid.
 * <P>
 * Beans which implement this class need only comply to the basic rules for
 * JavaBeans which means that all attributes should be encapsulated correctly 
 * with getters and setters.
 *
 * @author A. HARMEL-LAW. E: andrew.harmel.law@gmail.com
 */
public abstract class AbstractDataBean implements DataBeanIntf {

    /**
     * Returns a <String> containing the values of the requested properties
     * as a comma separated list
     * 
     * @param properties The bean attributes to include within the CSV String
     * @return The comma separated list of bean attributes
     * @throws IntrospectionException If one of the property names provided does 
     * not have a JavaBean getter method available
     */
    @Override
    public String toCsv(String[] properties) throws IntrospectionException {

        StringBuffer buffer = new StringBuffer();

        // Write the property values: ... "[property value]," ... as required
        buffer.append(toCharacterDelimitedList(properties, ',', DONT_WRAP_VARCHARS));

        return buffer.toString();
    }

    /**
     * Returns a <String> which reflects the values of the bean properties as a 
     * comma separated list
     * 
     * @return The comma separated list of bean properties
     * @throws IntrospectionException If one of the properties does not have a 
     * JavaBean getter method available
     */
    @Override
    public String toCsv() throws IntrospectionException {
        
        // Get all the properties on the bean as a String array
        String[] allProperties = getAllPropertyNamesAsArray();
        
        return toCsv(allProperties);
    }

    /**
     * Returns a <String> containing the values of the requested bean properties
     * as a character-delimited list
     *
     * @param properties The bean attributes to include within the pipe-delimited String
     * @param delimiterChar The character to use as a delimeter. e.g. a comma, or a pipe
     * @return The character-delimited list of bean attributes
     * @throws IntrospectionException If one of the property names provided does
     * not have a JavaBean getter method available
     */
    public String toCharacterDelimited(String[] properties, char delimiterChar) throws IntrospectionException {

        StringBuffer buffer = new StringBuffer();

        // Write the property values: ... "[property value]," ... as required
        buffer.append(toCharacterDelimitedList(properties, delimiterChar, DONT_WRAP_VARCHARS));

        return buffer.toString();
    }

    /**
     * Returns a <String> which reflects the values of the bean properties as a
     * character-delimited list
     *
     * @param delimiterChar The character to use as a delimeter. e.g. a comma, or a pipe
     * @return The char-delimited list of bean properties
     * @throws IntrospectionException If one of the properties does not have a
     * JavaBean getter method available
     */
    public String toCharacterDelimited(char delimiterChar) throws IntrospectionException {

        // Get all the properties on the bean as a String array
        String[] allProperties = getAllPropertyNamesAsArray();

        return toCharacterDelimited(allProperties, delimiterChar);
    }
    
    /**
     * Returns a <String> which reflects the values of the bean properties named
     * in the provided parameter as a SQL-99 INSERT statement
     * 
     * @param properties The properties to include within the SQL INSERT
     * @return The SQL-99 INSERT statement for the requested bean properties
     * @throws NoSuchFieldException If one of the property names provided does 
     * not exist as a property on the bean
     * @throws IntrospectionException If one of the property names provided does 
     * not have a JavaBean getter method available
     */
    @Override
    public String toSql(String[] properties) throws NoSuchFieldException, IntrospectionException {

        // TODO: parse fields to ensure that they are valid SQL
        
        StringBuffer buffer = new StringBuffer();
        
        // Write the start of the statement: "INSERT INTO [table name] (" ...
        buffer.append("INSERT INTO ");
        buffer.append(getSqlTableName());
        buffer.append('(');
        
        // Write the COLUMN NAME elements: ... "[column name]," ... as required
        int counter = 0;
        for (String fieldName : properties) {
            buffer.append(getPropertySqlColumnName(fieldName));
            if (counter < properties.length - 1) {
                buffer.append(", ");
            }
            counter++;
        }
        
        // Write the middle piece: ... ") VALUES (" ...
        buffer.append(") VALUES (");

        // Write the property values: ... "[property value]," ... as required
        buffer.append(toCharacterDelimitedList(properties, ',', true));

        // Write the statement end: ... ");"
        buffer.append(");");

        return buffer.toString();
    }
    
    /**
     * Returns a <String> which reflects the values of the bean properties as a 
     * SQL-99 INSERT statement
     * 
     * @return The SQL-99 INSERT statement for the requested bean properties
     * @throws NoSuchFieldException If one of the property names provided does 
     * not exist as a property on the bean
     * @throws IntrospectionException If one of the fields does not have a 
     * JavaBean getter method available
     */
    @Override
    public String toSql() throws NoSuchFieldException, IntrospectionException {
        
        // Get the names of all the properties on the bean as a <String> array
        String[] allProperties = getAllPropertyNamesAsArray();
        
        return toSql(allProperties);
    }
    
    /**
     * Validate the databean object property values.  Validation is based on the 
     * constraint annotations applied to the bean setters
     * 
     * @return The list of ConstraintViolations
     * @throws ValidationException If there is an exception thrown during 
     * validation.  It is wrapped in a ValidationException
     */
    public List validate() throws ValidationException {

        ConstrainedClass cc = ConstrainedClass.getConstrainedClass(this.getClass());
        return cc.validate(this);

    }

    /**
     * Gets the names of the properties in the databean as a <String> array
     * 
     * @return The <String> array of property names
     * @throws IntrospectionException If one of the properties does not have a 
     * JavaBean getter method available
     */
    private String[] getAllPropertyNamesAsArray() throws IntrospectionException {
        
        // Get info on all the bean's properties
        PropertyDescriptor[] descs 
                = Introspector.getBeanInfo(this.getClass()).getPropertyDescriptors();

        // For each Property...
        List<String> tempList = new ArrayList<String>();
        for (PropertyDescriptor desc : descs) {
            // Get the name of the current propetries getXxxx method and add to the array
            // Unless it's "class" (ignore that)
            if (!desc.getName().equals("class")) {
                tempList.add(desc.getName());
            }
        }
        
        return tempList.toArray(new String[0]);
    }
    
    /**
     * Return the name of the SQL-99 table that this databean represents, taken 
     * from it's @SqlTableName annotation.  If this is missing, the classname 
     * is used instead (in upper case with '_' seperating the words)
     * 
     * @return the name of SQL-99 table which this bean represents
     */
    private String getSqlTableName() {
        
        String sqlTableName;
        
        Annotation annotation = this.getClass().getAnnotation(SqlTableName.class);
        if (annotation == null) {
            sqlTableName = StringUtils.camelCapsToSqlName(this.getClass().getSimpleName());
        } else {
            sqlTableName = ((SqlTableName)annotation).value();
        }
        
        return sqlTableName;
    }

    /**
     * Return the name of the SQL-99 column that maps to the provided databean 
     * property. This is found by obtaining the @SqlColumnName annotation from the 
     * property.
     * <P>
     * If this annotation is missing, the property name itself is used instead (in 
     * upper case)
     * 
     * @return the name of SQL-99 column which the property represents
     * @throws NoSuchFieldException If the property specified does not exist
     */
    private String getPropertySqlColumnName(String propertyName) throws NoSuchFieldException {
    
        String sqlColumnName;
        
        Annotation annotation;
        try {
            Field field = this.getClass().getDeclaredField(propertyName);
            annotation = field.getAnnotation(SqlColumnName.class);
        } catch (NoSuchFieldException ex) {
            Logger.getLogger(AbstractDataBean.class.getName()).log(Level.SEVERE, null, ex);
            throw ex;
        }
        
        if (annotation == null) {
            sqlColumnName = StringUtils.camelCapsToSqlName(propertyName);
        } else {
            sqlColumnName = ((SqlColumnName)annotation).value();
        }
        
        return sqlColumnName;
    }
    
    /**
     * Gets the values of the requested attributes, and returns them as a comma
     * separated list
     * 
     * @param properties The names of the properties to include
     * @param wrapVarchars Whether or not to wrap varchars
     * @return The CSV list as requested
     * @throws IntrospectionException If one of the properties does not have a 
     * JavaBean getter method available
//     */
//    @SuppressWarnings(value = "unchecked")
//    private String toCommaSeparatedList(String[] properties, boolean wrapVarchars)
//            throws IntrospectionException {
//
//        StringBuffer buffer = new StringBuffer();
//
//        Class klass = this.getClass();
//
//        int counter = 0;
//        for (String attributeName : properties) {
//
//            Logger.getLogger(AbstractDataBean.class.getName()).log(Level.FINE,
//                    "current attribute: " + attributeName);
//
//            Method myMethod;
//            PropertyDescriptor desc = new PropertyDescriptor(attributeName, klass);
//
//            try {
//
//                Logger.getLogger(AbstractDataBean.class.getName()).log(Level.FINE,
//                    "current property descriptor: " + desc.getName());
//                myMethod = desc.getReadMethod();
//
//                Logger.getLogger(AbstractDataBean.class.getName()).log(Level.FINE,
//                    "current read method: " + myMethod.getName());
//
//                if (wrapVarchars && myMethod.getReturnType().getName().equals("java.lang.String")) {
//                    buffer.append('\'');
//                }
//
//                buffer.append(myMethod.invoke(this, new Object[0]));
//
//                if (wrapVarchars && myMethod.getReturnType().getName().equals("java.lang.String")) {
//                    buffer.append('\'');
//                }
//
//            } catch (IllegalArgumentException ex) {
//                Logger.getLogger(AbstractDataBean.class.getName()).log(Level.SEVERE, null, ex);
//            } catch (IllegalAccessException ex) {
//                Logger.getLogger(AbstractDataBean.class.getName()).log(Level.SEVERE, null, ex);
//            } catch (InvocationTargetException ex) {
//                Logger.getLogger(AbstractDataBean.class.getName()).log(Level.SEVERE, null, ex);
//            } catch (SecurityException ex) {
//                Logger.getLogger(AbstractDataBean.class.getName()).log(Level.SEVERE, null, ex);
//            }
//
//            if (counter < properties.length - 1) {
//                buffer.append(", ");
//            }
//
//            counter++;
//        }
//
//        return buffer.toString();
//    }

    /**
     * Gets the values of the requested attributes, and returns them as a
     * character-delimited list
     *
     * @param properties The names of the properties to include
     * @param delimiterChar The character to use as a delimeter. e.g. a comma, or a pipe
     * @param wrapVarchars Whether or not to wrap varchars
     * @return The pipe-delimited list as requested
     * @throws IntrospectionException If one of the properties does not have a
     * JavaBean getter method available
     */
    @SuppressWarnings(value = "unchecked")
    private String toCharacterDelimitedList(String[] properties, char delimiterChar,  boolean wrapVarchars)
            throws IntrospectionException {

        StringBuffer buffer = new StringBuffer();

        Class klass = this.getClass();

        int counter = 0;
        for (String attributeName : properties) {

            Logger.getLogger(AbstractDataBean.class.getName()).log(Level.FINE,
                    "current attribute: " + attributeName);

            Method myMethod;
            PropertyDescriptor desc = new PropertyDescriptor(attributeName, klass);

            try {

                Logger.getLogger(AbstractDataBean.class.getName()).log(Level.FINE,
                    "current property descriptor: " + desc.getName());
                myMethod = desc.getReadMethod();

                Logger.getLogger(AbstractDataBean.class.getName()).log(Level.FINE,
                    "current read method: " + myMethod.getName());

                if (wrapVarchars && myMethod.getReturnType().getName().equals("java.lang.String")) {
                    buffer.append('\'');
                }

                buffer.append(myMethod.invoke(this, new Object[0]));

                if (wrapVarchars && myMethod.getReturnType().getName().equals("java.lang.String")) {
                    buffer.append('\'');
                }

            } catch (IllegalArgumentException ex) {
                Logger.getLogger(AbstractDataBean.class.getName()).log(Level.SEVERE, null, ex);
            } catch (IllegalAccessException ex) {
                Logger.getLogger(AbstractDataBean.class.getName()).log(Level.SEVERE, null, ex);
            } catch (InvocationTargetException ex) {
                Logger.getLogger(AbstractDataBean.class.getName()).log(Level.SEVERE, null, ex);
            } catch (SecurityException ex) {
                Logger.getLogger(AbstractDataBean.class.getName()).log(Level.SEVERE, null, ex);
            }

            if (counter < properties.length - 1) {
                buffer.append(delimiterChar);
            }

            counter++;
        }

        return buffer.toString();
    }
}